package cn.juque.common.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import cn.juque.common.constants.MessageEnum;
import cn.juque.common.constants.StringConstants;
import cn.juque.common.exception.AppException;
import cn.juque.common.fastjson.abstracts.AbstractTypeReference;
import cn.juque.common.utils.dto.GetDTO;
import cn.juque.common.utils.dto.PostDTO;
import cn.juque.common.utils.dto.UploadDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author nuoka
 * @version 1.0.0
 * <li>IntelliJ IDEA</li>
 * <li>cn.juque.cn.juque.common.utils</li>
 * @date 2021/7/27 0:21
 **/
@Slf4j
public class HttpClientUtil {

    private static CloseableHttpClient httpClient = null;

    /**
     * 默认content 类型
     */
    private static final String DEFAULT_CONTENT_TYPE = "application/json";

    private static final String CHARSET = "UTF-8";

    private static final String DOWNLOADING = "downloading";

    /**
     * 默认请求超时时间30s
     */
    private static final int DEFAULT_TIME_OUT = 15000;

    private static final int COUNT = 50;

    private static final int TOTAL_COUNT = 100;

    private static final int HTTP_DEFAULT_KEEP_TIME = 15000;

    private HttpClientUtil() {
    }

    /**
     * 初始化连接池
     */
    private static synchronized void initPools() {
        if (httpClient == null) {
            // Http connection keepAlive 设置
            ConnectionKeepAliveStrategy defaultStrategy = (response, context) -> {
                HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && CommonStringUtil.equalsIgnoreCase("timeout", param)) {
                        try {
                            return Long.parseLong(value) * 1000;
                        } catch (Exception e) {
                            log.error("format KeepAlive timeout exception, exception:", e);
                        }
                    }
                }
                // keepTime
                return HTTP_DEFAULT_KEEP_TIME;
            };

            PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
            cm.setDefaultMaxPerRoute(COUNT);
            cm.setMaxTotal(TOTAL_COUNT);
            httpClient = HttpClients.custom().setKeepAliveStrategy(defaultStrategy).setConnectionManager(cm).build();
        }
    }

    /**
     * 执行http post请求 默认采用Content-Type：application/json，Accept：application/json
     *
     * @param postDto   请求结构体
     * @param reference 泛型
     * @return String
     */
    public static <T> T post(PostDTO postDto, AbstractTypeReference<T> reference) {
        long startTime = System.currentTimeMillis();
        String requestId = IdUtil.simpleUUID();
        log.debug("requestId={}, execute post param:{}", requestId, FastJsonUtil.toJsonString(postDto, postDto.getDateFormat()));
        String remoteUrl = joinParam(postDto.getUrl(), postDto.getParams());
        HttpEntityEnclosingRequestBase method = (HttpEntityEnclosingRequestBase) getRequest(remoteUrl, HttpPost.METHOD_NAME);
        mergeHeaders(method, postDto.getHeaders());
        method.setEntity(new StringEntity(FastJsonUtil.toJsonString(postDto.getPayload()), CharSequenceUtil.isEmpty(postDto.getCharset()) ? CHARSET : postDto.getCharset()));
        HttpContext context = HttpClientContext.create();
        try (CloseableHttpResponse httpResponse = httpClient.execute(method, context)) {
            HttpEntity httpEntity = httpResponse.getEntity();
            // 请求正常，并有返回值
            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK && null != httpEntity) {
                String responseBody = EntityUtils.toString(httpEntity, CharSequenceUtil.isEmpty(postDto.getCharset()) ? CHARSET : postDto.getCharset());
                log.debug("requestId={}, execute post response body:{}", requestId, responseBody);
                return FastJsonUtil.parseObject(responseBody, reference);
            }
            // 统一解析失败的日志
            responseFail(httpResponse, postDto, startTime);
        } catch (Exception e) {
            // 异常则强行中断请求，避免出现假死现象
            method.abort();
            log.error("requestId={}, execute post request exception, payload:{}, cost time(ms):{}", requestId, FastJsonUtil.toJsonString(postDto), System.currentTimeMillis() - startTime, e);
        }
        // 未能正常返回，则抛出异常
        throw new AppException(MessageEnum.SYSTEM_ERROR);
    }

    /**
     * 执行GET 请求
     *
     * @param getDTO    请求数据
     * @param reference 泛型
     * @return T
     */
    public static <T> T get(GetDTO getDTO, AbstractTypeReference<T> reference) {
        long startTime = System.currentTimeMillis();
        String requestId = IdUtil.simpleUUID();
        log.debug("requestId={}, execute get param:{}", requestId, FastJsonUtil.toJsonString(getDTO, getDTO.getDateFormat()));
        String remoteUrl = joinParam(getDTO.getUrl(), getDTO.getParam());
        HttpRequestBase method = getRequest(remoteUrl, HttpGet.METHOD_NAME);
        mergeHeaders(method, getDTO.getHeaders());
        HttpContext context = HttpClientContext.create();
        try (CloseableHttpResponse httpResponse = httpClient.execute(method, context)) {
            HttpEntity httpEntity = httpResponse.getEntity();
            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK && null != httpEntity) {
                String responseBody = EntityUtils.toString(httpEntity, CharSequenceUtil.isEmpty(getDTO.getCharset()) ? CHARSET : getDTO.getCharset());
                log.debug("requestId={}, execute get response body:{}", requestId, responseBody);
                return FastJsonUtil.parseObject(responseBody, reference);
            }
            // 统一解析失败的日志
            responseFail(httpResponse, getDTO, startTime);
        } catch (Exception e) {
            method.abort();
            log.error("requestId={}, execute get request exception, payload:{},cost time(ms):{}", requestId, FastJsonUtil.toJsonString(getDTO), System.currentTimeMillis() - startTime, e);
        }
        // 未能正常返回，则抛出异常
        throw new AppException(MessageEnum.SYSTEM_ERROR);
    }

    /**
     * 文件下载
     *
     * @param getDTO   GetDTO
     * @param filePath 本地文件路径
     */
    public static Boolean download(GetDTO getDTO, String filePath) {
        String remoteUrl = joinParam(getDTO.getUrl(), getDTO.getParam());
        HttpURLConnection connection = null;
        try {
            URL url = new URL(remoteUrl);
            connection = (HttpURLConnection) url.openConnection();
            if (MapUtil.isNotEmpty(getDTO.getHeaders())) {
                for (Map.Entry<String, String> entry : getDTO.getHeaders().entrySet()) {
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }
            String absolutePath = CharSequenceUtil.subBefore(filePath, StringConstants.POINT, true);
            File downloadFile = FileUtil.file(absolutePath + StringConstants.POINT + DOWNLOADING);
            FileUtil.del(downloadFile);
            try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
                 OutputStream outputStream = Files.newOutputStream(downloadFile.toPath())) {
                byte[] bytes = new byte[1024 * 1024 * 2];
                int len;
                while ((len = inputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
            }
            if (!FileUtil.exist(downloadFile)) {
                return false;
            }
            File file = FileUtil.file(filePath);
            FileUtil.del(file);
            FileUtil.copy(downloadFile, file, true);
            FileUtil.del(downloadFile);
            return true;
        } catch (Exception e) {
            log.error("url:{}, 文件下载异常", remoteUrl);
        } finally {
            if (Objects.nonNull(connection)) {
                connection.disconnect();
            }
        }
        return false;
    }

    /**
     * 文件上传
     *
     * @param uploadDTO DTO
     * @param reference 返回类型
     * @param <T>       T
     * @return T
     */
    public static <T> T upload(UploadDTO uploadDTO, AbstractTypeReference<T> reference) {
        long startTime = System.currentTimeMillis();
        MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
        // 避免中文文件名乱码
        multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        multipartEntityBuilder.setCharset(StandardCharsets.UTF_8);
        multipartEntityBuilder.setContentType(ContentType.MULTIPART_FORM_DATA);
        // 设置参数
        if (MapUtil.isNotEmpty(uploadDTO.getParams())) {
            uploadDTO.getParams().forEach((k, v) ->
                    multipartEntityBuilder.addTextBody(k, v, ContentType.create("text/plain", StandardCharsets.UTF_8)));
        }
        if(!FileUtil.exist(uploadDTO.getFilePath())) {
            throw new AppException(MessageEnum.PARAM_ERROR.getCode(), "源文件不存在");
        }
        // 设置文件
        multipartEntityBuilder.addBinaryBody(uploadDTO.getFileKey(), FileUtil.file(uploadDTO.getFilePath()), ContentType.DEFAULT_BINARY, uploadDTO.getFileName());
        HttpEntityEnclosingRequestBase method = (HttpEntityEnclosingRequestBase) getRequest(uploadDTO.getUrl(), HttpPost.METHOD_NAME);
        method.removeHeaders("Content-Type");
        method.removeHeaders("Accept");
        mergeHeaders(method, uploadDTO.getHeaders());
        method.setEntity(multipartEntityBuilder.build());
        HttpContext context = HttpClientContext.create();
        try (CloseableHttpResponse httpResponse = httpClient.execute(method, context)) {
            HttpEntity httpEntity = httpResponse.getEntity();
            // 请求正常，并有返回值
            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK && null != httpEntity) {
                String responseBody = EntityUtils.toString(httpEntity, CHARSET);
                return FastJsonUtil.parseObject(responseBody, reference);
            }
            // 统一解析失败的日志
            responseFail(httpResponse, "{}", startTime);
        } catch (Exception e) {
            method.abort();
            log.error("execute upload file exception, cost time(ms):{}", System.currentTimeMillis() - startTime, e);
        }
        // 未能正常返回，则抛出异常
        throw new AppException(MessageEnum.SYSTEM_ERROR);
    }

    /**
     * 创建请求
     *
     * @param uri        请求url
     * @param methodName 请求的方法类型
     * @return HttpRequestBase
     */
    private static HttpRequestBase getRequest(String uri, String methodName) {
        if (httpClient == null) {
            initPools();
        }
        HttpRequestBase method = new HttpPost(uri);
        RequestConfig requestConfig = RequestConfig.custom().
                setSocketTimeout(DEFAULT_TIME_OUT).
                setConnectTimeout(DEFAULT_TIME_OUT).
                setConnectionRequestTimeout(DEFAULT_TIME_OUT).
                setExpectContinueEnabled(false).build();

        if (HttpPut.METHOD_NAME.equalsIgnoreCase(methodName)) {
            method = new HttpPut(uri);
        } else if (HttpPost.METHOD_NAME.equalsIgnoreCase(methodName)) {
            method = new HttpPost(uri);
        } else if (HttpGet.METHOD_NAME.equalsIgnoreCase(methodName)) {
            method = new HttpGet(uri);
        }
        method.addHeader("Content-Type", DEFAULT_CONTENT_TYPE);
        method.addHeader("Accept", DEFAULT_CONTENT_TYPE);
        method.setConfig(requestConfig);
        return method;
    }

    /**
     * url 拼接参数
     *
     * @param url   请求路径
     * @param param 参数
     * @return String
     */
    public static String joinParam(String url, Map<String, Object> param) {
        if (CollUtil.isEmpty(param)) {
            return url;
        }
        StringBuilder stringBuilder = new StringBuilder(url);
        if (CharSequenceUtil.contains(url, StringConstants.MARK)) {
            stringBuilder.append(StringConstants.AND);
        } else {
            stringBuilder.append(StringConstants.MARK);
        }
        param.forEach((key, value) -> stringBuilder.append(key).append(StringConstants.EQUAL).append(value).append(StringConstants.AND));
        String remoteUrl = stringBuilder.toString();
        return CharSequenceUtil.sub(remoteUrl, 0, remoteUrl.length() - 1);
    }

    /**
     * 分割参数
     *
     * @param url url
     * @return map
     */
    public static Map<String, String> splitParam(String url) {
        if (CharSequenceUtil.isEmpty(url)) {
            return new HashMap<>(0);
        }
        String params = CharSequenceUtil.contains(url, StringConstants.MARK) ?
                CharSequenceUtil.subAfter(url, StringConstants.MARK, false) : url;
        List<String> list = CharSequenceUtil.split(params, StringConstants.AND);
        if (CollUtil.isEmpty(list)) {
            return new HashMap<>(0);
        }
        return list.stream()
                .collect(Collectors.toMap(
                        t -> CharSequenceUtil.trim(CharSequenceUtil.subBefore(t, StringConstants.EQUAL, false)),
                        t -> CharSequenceUtil.trim(CharSequenceUtil.subAfter(t, StringConstants.EQUAL, false)), (k1, k2) -> k2));
    }

    /**
     * 统一记录请求失败的日志，即status ！= 200
     *
     * @param httpResponse 响应结果
     * @param body         请求主体
     * @param startTime    请求开始时间
     * @throws IOException IOException
     */
    private static void responseFail(CloseableHttpResponse httpResponse, Object body, long startTime) throws IOException {
        HttpEntity httpEntity = httpResponse.getEntity();
        if (null == httpEntity) {
            log.error("http entity is null, body:{},cost time(ms):{}", FastJsonUtil.toJsonString(body), System.currentTimeMillis() - startTime);
            return;
        }
        String responseBody = EntityUtils.toString(httpEntity, CHARSET);
        log.error("http client execute fail, body:{}, response:{}", FastJsonUtil.toJsonString(body), responseBody);
    }

    /**
     * 合并请求头
     *
     * @param requestBase HttpEntityEnclosingRequestBase
     * @param headers     Map
     */
    private static void mergeHeaders(HttpRequestBase requestBase, Map<String, String> headers) {
        if (MapUtil.isEmpty(headers)) {
            return;
        }
        headers.forEach(requestBase::addHeader);
    }
}
